import os
from typing import Union, Optional, List

from redis.typing import EncodableT, ExpiryT
from redis.asyncio import Redis, RedisCluster
from redis.asyncio.cluster import ClusterNode
from redis.asyncio.connection import ConnectionPool

from conf.const import EnvConst
from config import RedisConfig

REDIS_CONNECTION_PARAMS = {
    'host': RedisConfig.HOSTS[0],
    'port': RedisConfig.PORTS[0],
    'password': RedisConfig.PASSWD,
    'socket_timeout': RedisConfig.SOCKET_TIMEOUT,
    'socket_connect_timeout': RedisConfig.SOCKET_CONNECT_TIMEOUT,
    'socket_keepalive': True,
    'encoding': 'utf-8',
    'decode_responses': True,
    'max_connections': RedisConfig.MAX_CONNECTIONS,
    'username': RedisConfig.USER
}


class Pool:
    _instances = {}

    @classmethod
    def pool(cls, db: int = 0):
        if db in cls._instances:
            return cls._instances.get(db)

        client = ConnectionPool(db=db, **REDIS_CONNECTION_PARAMS)
        cls._instances[db] = client
        return client


class ClusterPool:
    _instance = None

    def __init__(self):
        pass

    @classmethod
    def pool(cls):
        if cls._instance:
            return cls._instance

        nodes = []
        for host, port in zip(RedisConfig.HOSTS, RedisConfig.PORTS):
            node = ClusterNode(host=host, port=port)
            nodes.append(node)

        client = RedisCluster(startup_nodes=nodes, read_from_replicas=True, **REDIS_CONNECTION_PARAMS)
        cls._instance = client
        return client


class BaseRedis(object):
    DB = 0

    def __init__(self) -> None:
        self._name = None

        if os.environ.get('CODE_ENV') == EnvConst.TEST:
            self.client: Redis = Redis(connection_pool=ConnectionPool(db=self.DB, **REDIS_CONNECTION_PARAMS))
        else:
            if RedisConfig.IS_CLUSTER:
                self.client = ClusterPool.pool()
            else:
                self.client: Redis = Redis(connection_pool=Pool.pool(self.DB))

    def expire(self, name: str, seconds: ExpiryT, nx: bool = False, xx: bool = False, gt: bool = False,
               lt: bool = False):
        """
        Set an expired flag on key `name` for `time` seconds. `time`
        can be represented by an integer or a Python timedelta object.

            NX -> Set expiry only when the key has no expiry
            XX -> Set expiry only when the key has an existing expiry
            GT -> Set expiry only when the new expiry is greater than current one
            LT -> Set expiry only when the new expiry is less than current one
        """

        return self.client.expire(name=name, time=seconds, nx=nx, xx=xx, gt=gt, lt=lt)

    def ttl(self, name: str):
        """Returns the number of seconds until the key ``name`` will expire"""

        return self.client.ttl(name=name)

    def delete(self, name: str):
        """Delete one key specified by `names`"""

        return self.client.delete(name)

    def deletes(self, names: List[str]):
        """Delete more keys specified by `names`"""

        return self.client.delete(*names)

    def exists(self, name: str):
        """Returns the number of `names` that exist"""

        return self.client.exists(name)

    def keys(self, pattern):
        """获取 keys"""
        return self.client.keys(pattern)

    def get(self, name: str):
        """
        Return the value at key `name`, or None if the key doesn't exist
        """

        return self.client.get(name=name)

    def _incr_by(self, name: str, amount: int = 1):
        """value += amount"""

        return self.client.incrby(name=name, amount=amount)

    def set(
            self,
            name: str,
            value: EncodableT,
            ex: Union[ExpiryT, None] = None,
            px: Union[ExpiryT, None] = None,
            nx: bool = False,
            xx: bool = False,
            get: bool = False
    ):
        """Set the value at key `name` to `value`

        `ex` sets an expired flag on key `name` for `ex` seconds.

        `px` sets an expired flag on key `name` for `px` milliseconds.

        `nx` if set to True, set the value at key `name` to `value` only
            if it does not exist.

        `xx` if set to True, set the value at key `name` to `value` only
            if it already exists.

        `get` if True, set the value at key `name` to `value` and return
            the old value stored at key, or None if the key did not exist.
            (Available since Redis 6.2)
        """

        return self.client.set(name=name, value=value, ex=ex, px=px, nx=nx, xx=xx, get=get)

    def _hash_set(
            self, name: str, key: Optional[str] = None, value: Optional[str] = None, mapping: Optional[dict] = None
    ):
        """
        Set `key` to `value` within hash `name`,
        `mapping` accepts a dict of key/value pairs that that will be
        added to hash `name`.
        Returns the number of fields that were added.
        """

        return self.client.hset(name=name, key=key, value=value, mapping=mapping)

    async def _hash_get_values(self, name: str, keys: Optional[list] = None):
        """hash, Returns a list of values ordered identically to `keys`"""

        if not keys:
            return await self.client.hgetall(name=name)

        response = await self.client.hmget(name=name, keys=keys)

        result = dict()
        for index, key in enumerate(keys):
            result[key] = response[index]

        return result

    def _hash_del_key(self, name: str, keys: list):
        """hash, Delete `keys` from hash `name`

        have * need list         ([key, key, key])
            hdel(*[key, key, key])
        no   * need multiple key (key, key, key)
            hdel(key, key, key)
        """

        self.client.hdel(name, *keys)

    def _lis_push(self, name: str, values: list, is_right: bool = True):
        """list, Push `values` into the head or tail of the list `name`, depend on is_right flag"""

        if is_right:
            return self.client.rpush(name, *values)
        else:
            return self.client.lpush(name, *values)

    def _list_left_range(self, name: str, start: int = 0, end: int = -1):
        """list, Return a slice of the list `name` between position `start` and `end`"""

        return self.client.lrange(name=name, start=start, end=end)

    def _list_set(self, name: str, index: int, value: str):
        """list, Set element at `index` of list `name` to `value`"""

        return self.client.lset(name=name, index=index, value=value)
